// © Broadcom. All Rights Reserved.
// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
// SPDX-License-Identifier: Apache-2.0

package object

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"log"
	"path"
	"strings"

	"github.com/vmware/govmomi/vim25/progress"
	"github.com/vmware/govmomi/vim25/soap"
)

// DatastoreFileManager combines FileManager and VirtualDiskManager to manage files on a Datastore
type DatastoreFileManager struct {
	Datacenter         *Datacenter
	Datastore          *Datastore
	FileManager        *FileManager
	VirtualDiskManager *VirtualDiskManager

	Force            bool
	DatacenterTarget *Datacenter
}

// NewFileManager creates a new instance of DatastoreFileManager
func (d Datastore) NewFileManager(dc *Datacenter, force bool) *DatastoreFileManager {
	c := d.Client()

	m := &DatastoreFileManager{
		Datacenter:         dc,
		Datastore:          &d,
		FileManager:        NewFileManager(c),
		VirtualDiskManager: NewVirtualDiskManager(c),
		Force:              force,
		DatacenterTarget:   dc,
	}

	return m
}

func (m *DatastoreFileManager) WithProgress(ctx context.Context, s progress.Sinker) context.Context {
	return context.WithValue(ctx, m, s)
}

func (m *DatastoreFileManager) wait(ctx context.Context, task *Task) error {
	var logger progress.Sinker
	if s, ok := ctx.Value(m).(progress.Sinker); ok {
		logger = s
	}
	_, err := task.WaitForResult(ctx, logger)
	return err
}

// Delete dispatches to the appropriate Delete method based on file name extension
func (m *DatastoreFileManager) Delete(ctx context.Context, name string) error {
	switch path.Ext(name) {
	case ".vmdk":
		return m.DeleteVirtualDisk(ctx, name)
	default:
		return m.DeleteFile(ctx, name)
	}
}

// DeleteFile calls FileManager.DeleteDatastoreFile
func (m *DatastoreFileManager) DeleteFile(ctx context.Context, name string) error {
	p := m.Path(name)

	task, err := m.FileManager.DeleteDatastoreFile(ctx, p.String(), m.Datacenter)
	if err != nil {
		return err
	}

	return m.wait(ctx, task)
}

// DeleteVirtualDisk calls VirtualDiskManager.DeleteVirtualDisk
// Regardless of the Datastore type, DeleteVirtualDisk will fail if 'ddb.deletable=false',
// so if Force=true this method attempts to set 'ddb.deletable=true' before starting the delete task.
func (m *DatastoreFileManager) DeleteVirtualDisk(ctx context.Context, name string) error {
	p := m.Path(name)

	var merr error

	if m.Force {
		merr = m.markDiskAsDeletable(ctx, p)
	}

	task, err := m.VirtualDiskManager.DeleteVirtualDisk(ctx, p.String(), m.Datacenter)
	if err != nil {
		log.Printf("markDiskAsDeletable(%s): %s", p, merr)
		return err
	}

	return m.wait(ctx, task)
}

// CopyFile calls FileManager.CopyDatastoreFile
func (m *DatastoreFileManager) CopyFile(ctx context.Context, src string, dst string) error {
	srcp := m.Path(src)
	dstp := m.Path(dst)

	task, err := m.FileManager.CopyDatastoreFile(ctx, srcp.String(), m.Datacenter, dstp.String(), m.DatacenterTarget, m.Force)
	if err != nil {
		return err
	}

	return m.wait(ctx, task)
}

// Copy dispatches to the appropriate FileManager or VirtualDiskManager Copy method based on file name extension
func (m *DatastoreFileManager) Copy(ctx context.Context, src string, dst string) error {
	srcp := m.Path(src)
	dstp := m.Path(dst)

	f := m.FileManager.CopyDatastoreFile

	if srcp.IsVMDK() {
		// types.VirtualDiskSpec=nil as it is not implemented by vCenter
		f = func(ctx context.Context, src string, srcDC *Datacenter, dst string, dstDC *Datacenter, force bool) (*Task, error) {
			return m.VirtualDiskManager.CopyVirtualDisk(ctx, src, srcDC, dst, dstDC, nil, force)
		}
	}

	task, err := f(ctx, srcp.String(), m.Datacenter, dstp.String(), m.DatacenterTarget, m.Force)
	if err != nil {
		return err
	}

	return m.wait(ctx, task)
}

// MoveFile calls FileManager.MoveDatastoreFile
func (m *DatastoreFileManager) MoveFile(ctx context.Context, src string, dst string) error {
	srcp := m.Path(src)
	dstp := m.Path(dst)

	task, err := m.FileManager.MoveDatastoreFile(ctx, srcp.String(), m.Datacenter, dstp.String(), m.DatacenterTarget, m.Force)
	if err != nil {
		return err
	}

	return m.wait(ctx, task)
}

// Move dispatches to the appropriate FileManager or VirtualDiskManager Move method based on file name extension
func (m *DatastoreFileManager) Move(ctx context.Context, src string, dst string) error {
	srcp := m.Path(src)
	dstp := m.Path(dst)

	f := m.FileManager.MoveDatastoreFile

	if srcp.IsVMDK() {
		f = m.VirtualDiskManager.MoveVirtualDisk
	}

	task, err := f(ctx, srcp.String(), m.Datacenter, dstp.String(), m.DatacenterTarget, m.Force)
	if err != nil {
		return err
	}

	return m.wait(ctx, task)
}

// Path converts path name to a DatastorePath
func (m *DatastoreFileManager) Path(name string) *DatastorePath {
	var p DatastorePath

	if !p.FromString(name) {
		p.Path = name
		p.Datastore = m.Datastore.Name()
	}

	return &p
}

func (m *DatastoreFileManager) markDiskAsDeletable(ctx context.Context, path *DatastorePath) error {
	r, _, err := m.Datastore.Download(ctx, path.Path, &soap.DefaultDownload)
	if err != nil {
		return err
	}

	defer r.Close()

	hasFlag := false
	buf := new(bytes.Buffer)

	s := bufio.NewScanner(&io.LimitedReader{R: r, N: 2048}) // should be only a few hundred bytes, limit to be sure

	for s.Scan() {
		line := s.Text()
		if strings.HasPrefix(line, "ddb.deletable") {
			hasFlag = true
			continue
		}

		fmt.Fprintln(buf, line)
	}

	if err := s.Err(); err != nil {
		return err // any error other than EOF
	}

	if !hasFlag {
		return nil // already deletable, so leave as-is
	}

	// rewrite the .vmdk with ddb.deletable flag removed (the default is true)
	return m.Datastore.Upload(ctx, buf, path.Path, &soap.DefaultUpload)
}
